Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ

Node / meshtastic / Meshtastic-Android / files / docs / BUILD_LOGIC_CONVENTIONS_GUIDE.md

Displaying Raw β€’ View rendered β€’ Download

docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md f07624be882e679affc02931dc75e8cb1594de18 (f07624be) Text, 10.29 KB

Tc9d1d9# Build-Logic Convention Patterns & Guidelines

Quick reference for maintaining and extending the build-logic convention system.

Tc9d1d9## Core Principles

Tff7b721. **DRY (Don't Repeat Yourself)**: Extract common configuration into functions
Tff7b722. **Clarity Over Cleverness**: Explicit intent in Ta5d6ff`build.gradle.kts` files matters
Tff7b723. **Single Responsibility**: Each convention plugin has one clear purpose
Tff7b724. **Test-Driven**: Configuration changes must pass Ta5d6ff`spotlessCheck`, Ta5d6ff`detekt`, and tests

Tc9d1d9## Convention Plugin Architecture

Ta5d6ff```
build-logic/
β”œβ”€β”€ convention/
β”‚ β”œβ”€β”€ src/main/kotlin/
β”‚ β”‚ β”œβ”€β”€ KmpFeatureConventionPlugin.kt # KMP feature modules (composes library + compose + koin + common deps)
β”‚ β”‚ β”œβ”€β”€ KmpLibraryConventionPlugin.kt # KMP modules: core libraries
β”‚ β”‚ β”œβ”€β”€ KmpLibraryComposeConventionPlugin.kt # KMP Compose Multiplatform setup
β”‚ β”‚ β”œβ”€β”€ KmpJvmAndroidConventionPlugin.kt # Opt-in jvmAndroidMain hierarchy for Android + desktop JVM
β”‚ β”‚ β”œβ”€β”€ AndroidApplicationConventionPlugin.kt # Main app
β”‚ β”‚ β”œβ”€β”€ AndroidLibraryConventionPlugin.kt # Android-only libraries
β”‚ β”‚ β”œβ”€β”€ AndroidApplicationComposeConventionPlugin.kt
β”‚ β”‚ β”œβ”€β”€ AndroidLibraryComposeConventionPlugin.kt
β”‚ β”‚ β”œβ”€β”€ org/meshtastic/buildlogic/
β”‚ β”‚ β”‚ β”œβ”€β”€ KotlinAndroid.kt # Base Kotlin/Android config
β”‚ β”‚ β”‚ β”œβ”€β”€ AndroidCompose.kt # Compose setup
β”‚ β”‚ β”‚ β”œβ”€β”€ FlavorResolution.kt # Flavor configuration
β”‚ β”‚ β”‚ β”œβ”€β”€ MeshtasticFlavor.kt # Flavor definitions
β”‚ β”‚ β”‚ β”œβ”€β”€ Detekt.kt # Static analysis
β”‚ β”‚ β”‚ β”œβ”€β”€ Spotless.kt # Code formatting
β”‚ β”‚ β”‚ └── ... (other config modules)
```

Tc9d1d9## How to Add a New Convention

Tc9d1d9### Example: Adding a new test framework dependency

**Current Pattern (GOOD βœ…):**

If all KMP modules need a dependency, add it to Ta5d6ff`KotlinAndroid.kt::configureKmpTestDependencies()`:

Ta5d6ff```Ta5d6ffkotlin
Tff7b72internal Tff7b72fun Te6edf3ProjectTb4b4b4.Td2a8ffconfigureKmpTestDependenciesTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3KotlinMultiplatformExtensionTff7b72> Tb4b4b4{
Te6edf3sourceSetsTb4b4b4.Te6edf3apply Tb4b4b4{
Tff7b72val Te6edf3commonTest Tff7b72= Te6edf3findByNameTb4b4b4(Ta5d6ff"Ta5d6ffcommonTestTa5d6ff"Tb4b4b4) Tff7b72?: Tff7b72returnTf0883e@apply
Te6edf3commonTestTb4b4b4.Te6edf3dependencies Tb4b4b4{
Te6edf3implementationTb4b4b4(Te6edf3kotlinTb4b4b4(Ta5d6ff"Ta5d6fftestTa5d6ff"Tb4b4b4)Tb4b4b4)
T8b949e// NEW: Add here once, applies to all ~15 KMP modules
Te6edf3implementationTb4b4b4(Te6edf3libsTb4b4b4.Te6edf3libraryTb4b4b4(Ta5d6ff"Ta5d6ffnew-test-frameworkTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
T8b949e// ... androidHostTest setup
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```

**Result:** All 15 feature and core modules automatically get the dependency βœ…

Tc9d1d9### Example: Adding shared `jvmAndroidMain` code to a KMP module

**Current Pattern (GOOD βœ…):**

If a KMP module needs Java/JVM APIs shared between Android and desktop JVM, apply the opt-in convention plugin instead of manually creating source sets and Ta5d6ff`dependsOn(...)` edges:

Ta5d6ff```Ta5d6ffkotlin
Te6edf3plugins Tb4b4b4{
Te6edf3aliasTb4b4b4(Te6edf3libsTb4b4b4.Te6edf3pluginsTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3kmpTb4b4b4.Te6edf3libraryTb4b4b4)
Te6edf3idTb4b4b4(Ta5d6ff"Ta5d6ffmeshtastic.kmp.jvm.androidTa5d6ff"Tb4b4b4)
Tb4b4b4}

Te6edf3kotlin Tb4b4b4{
Te6edf3jvmTb4b4b4(Tb4b4b4)
Te6edf3android Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}

Te6edf3sourceSets Tb4b4b4{
Te6edf3commonMainTb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Te6edf3jvmMainTb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* jvm-only additions */ Tb4b4b4}
Te6edf3androidMainTb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* android-only additions */ Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```

**Why:** The convention uses Kotlin's hierarchy template API to create Ta5d6ff`jvmAndroidMain` without the Ta5d6ff`Default Kotlin Hierarchy Template Not Applied Correctly` warning triggered by hand-written Ta5d6ff`dependsOn(...)` graphs.

Tc9d1d9### Example: Creating a new KMP feature module

**Current Pattern (GOOD βœ…):**

Use Ta5d6ff`meshtastic.kmp.feature` for any Ta5d6ff`feature:*` module. It composes Ta5d6ff`kmp.library` + Ta5d6ff`kmp.library.compose` + Ta5d6ff`koin` and provides all the common Compose/Lifecycle/Koin/Android dependencies that every feature needs:

Ta5d6ff```Ta5d6ffkotlin
Te6edf3plugins Tb4b4b4{
Te6edf3aliasTb4b4b4(Te6edf3libsTb4b4b4.Te6edf3pluginsTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3kmpTb4b4b4.Te6edf3featureTb4b4b4)
T8b949e// Optional: add only if this feature needs serialization
Te6edf3aliasTb4b4b4(Te6edf3libsTb4b4b4.Te6edf3pluginsTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3kotlinxTb4b4b4.Te6edf3serializationTb4b4b4)
Tb4b4b4}

Te6edf3kotlin Tb4b4b4{
Te6edf3jvmTb4b4b4(Tb4b4b4)
Te6edf3android Tb4b4b4{
Te6edf3namespace Tff7b72= Ta5d6ff"Ta5d6fforg.meshtastic.feature.yourfeatureTa5d6ff"
Te6edf3androidResourcesTb4b4b4.Te6edf3enable Tff7b72= Tff7b72false
Te6edf3withHostTest Tb4b4b4{ Te6edf3isIncludeAndroidResources Tff7b72= Tff7b72true Tb4b4b4}
Tb4b4b4}

Te6edf3sourceSets Tb4b4b4{
Te6edf3commonMainTb4b4b4.Te6edf3dependencies Tb4b4b4{
T8b949e// Only module-SPECIFIC deps here
Te6edf3implementationTb4b4b4(Te6edf3projectsTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3commonTb4b4b4)
Te6edf3implementationTb4b4b4(Te6edf3projectsTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3modelTb4b4b4)
Te6edf3implementationTb4b4b4(Te6edf3projectsTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3uiTb4b4b4)
Tb4b4b4}
Te6edf3androidMainTb4b4b4.Te6edf3dependencies Tb4b4b4{
T8b949e// Only Android-specific extras here
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```

**What the plugin provides automatically:**
Tff7b72- Ta5d6ff`commonMain`: Ta5d6ff`compose-multiplatform-material3`, Ta5d6ff`compose-multiplatform-materialIconsExtended`, Ta5d6ff`jetbrains-lifecycle-viewmodel-compose`, Ta5d6ff`koin-compose-viewmodel`, Ta5d6ff`kermit`
Tff7b72- Ta5d6ff`androidMain`: Ta5d6ff`androidx-compose-bom` (platform), Ta5d6ff`accompanist-permissions`, Ta5d6ff`androidx-activity-compose`, Ta5d6ff`androidx-compose-material3`, Ta5d6ff`androidx-compose-material-iconsExtended`, Ta5d6ff`androidx-compose-ui-text`, Ta5d6ff`androidx-compose-ui-tooling-preview`
Tff7b72- Ta5d6ff`commonTest`: Ta5d6ff`core:testing`

**Why:** Eliminates ~15 duplicate dependency declarations per feature module (modelled after Now in Android's Ta5d6ff`AndroidFeatureImplConventionPlugin`).

Tc9d1d9### Example: Adding Android-specific test config

**Pattern:** Add to Ta5d6ff`AndroidLibraryConventionPlugin.kt`:

Ta5d6ff```Ta5d6ffkotlin
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3LibraryExtensionTff7b72> Tb4b4b4{
Te6edf3configureKotlinAndroidTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3testOptionsTb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3animationsDisabled Tff7b72= Tff7b72true
T8b949e// NEW: Android-specific test config
Te6edf3unitTestsTb4b4b4.Te6edf3isIncludeAndroidResources Tff7b72= Tff7b72true
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```

**Alternative:** If it applies to both app and library, consider extracting a function:

Ta5d6ff```Ta5d6ffkotlin
Tff7b72internal Tff7b72fun Te6edf3ProjectTb4b4b4.Td2a8ffconfigureAndroidTestOptionsTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3CommonExtensionTff7b72> Tb4b4b4{
Te6edf3testOptionsTb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3animationsDisabled Tff7b72= Tff7b72true
T8b949e// Shared test options
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```

Tc9d1d9## Duplication Heuristics

**When to consolidate (DRY):**
Tff7b72- βœ… Configuration appears in 3+ convention plugins
Tff7b72- βœ… The duplication changes together (same reasons to update)
Tff7b72- βœ… Extraction doesn't require complex type gymnastics
Tff7b72- βœ… Underlying Gradle extension is the same (Ta5d6ff`CommonExtension`)

**When to keep separate (Clarity):**
Tff7b72- βœ… Different Gradle extension types (Ta5d6ff`ApplicationExtension` vs Ta5d6ff`LibraryExtension`)
Tff7b72- βœ… Plugin intent is explicit in Ta5d6ff`build.gradle.kts` usage
Tff7b72- βœ… Duplication is small (<50 lines) and stable
Tff7b72- βœ… Future divergence between app/library handling is plausible

**Examples in codebase:**

| Duplication | Status | Reasoning |
|-------------|--------|-----------|
| Ta5d6ff`AndroidApplicationComposeConventionPlugin` β‰ˆ Ta5d6ff`AndroidLibraryComposeConventionPlugin` | **Kept Separate** | Different extension types; small duplication; explicit intent |
| Ta5d6ff`AndroidApplicationFlavorsConventionPlugin` β‰ˆ Ta5d6ff`AndroidLibraryFlavorsConventionPlugin` | **Kept Separate** | Different extension types; small duplication; explicit intent |
| Ta5d6ff`configureKmpTestDependencies()` (7 modules) | **Consolidated** | Large duplication; single source of truth; all KMP modules benefit |
| Ta5d6ff`jvmAndroidMain` hierarchy setup (4 modules) | **Consolidated** | Shared KMP hierarchy pattern; avoids manual Ta5d6ff`dependsOn(...)` edges and hierarchy warnings |

Tc9d1d9## Testing Convention Changes

After modifying a convention plugin, verify:

Ta5d6ff```Ta5d6ffbash
T8b949e# 1. Code quality
./gradlew spotlessCheck detekt

T8b949e# 2. Compilation
./gradlew assembleDebug assembleRelease

T8b949e# 3. Tests
./gradlew Tffa657test T8b949e# All unit tests
./gradlew :feature:messaging:jvmTest T8b949e# Feature module tests
./gradlew :feature:node:testAndroidHostTest T8b949e# Android host tests
Ta5d6ff```

Tc9d1d9## Documentation Requirements

When you add/modify a convention:

Tff7b721. **Add Kotlin docs** to the function:
Ta5d6ff ```Ta5d6ffkotlin
T8b949e/**
* Configure test dependencies for KMP modules.
*
* Automatically applies kotlin("test") to:
* - commonTest source set (all targets)
* - androidHostTest source set (Android-only)
*
* Usage: Called automatically by KmpLibraryConventionPlugin
*/
Tff7b72internal Tff7b72fun Te6edf3ProjectTb4b4b4.Td2a8ffconfigureKmpTestDependenciesTb4b4b4(Tb4b4b4) Tb4b4b4{ Tb4b4b4.Tb4b4b4.Tb4b4b4. Tb4b4b4}
Ta5d6ff ```

Tff7b722. **Update AGENTS.md** if convention affects developers
Tff7b723. **Update this guide** if pattern changes

Tc9d1d9## Performance Tips

Tff7b72- **Configuration-time:** Convention logic runs during Gradle configuration (0.5-2s)
Tff7b72- **Build-time:** No impact (conventions don't execute tasks)
Tff7b72- **Optimization focus:** Minimize Ta5d6ff`extensions.configure()` blocks (lazy evaluation is preferred)

Tc9d1d9### Good βœ…
Ta5d6ff```Ta5d6ffkotlin
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3KotlinMultiplatformExtensionTff7b72> Tb4b4b4{
T8b949e// Single block for all source set configuration
Te6edf3sourceSetsTb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3commonTestTb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Te6edf3androidHostTestTff7b72?.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```

Tc9d1d9### Avoid ❌
Ta5d6ff```Ta5d6ffkotlin
T8b949e// Multiple blocks - slower configuration
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3KotlinMultiplatformExtensionTff7b72> Tb4b4b4{
Te6edf3sourceSetsTb4b4b4.Te6edf3getByNameTb4b4b4(Ta5d6ff"Ta5d6ffcommonTestTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Tb4b4b4}
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3KotlinMultiplatformExtensionTff7b72> Tb4b4b4{
Te6edf3sourceSetsTb4b4b4.Te6edf3getByNameTb4b4b4(Ta5d6ff"Ta5d6ffandroidHostTestTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Tb4b4b4}
Ta5d6ff```

Tc9d1d9## Common Pitfalls

Tc9d1d9### ❌ **Mistake: Adding dependencies in the wrong place**
Ta5d6ff```Ta5d6ffkotlin
T8b949e// WRONG: Adds to ALL modules, not just KMP
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3ProjectTff7b72> Tb4b4b4{
Te6edf3dependencies Tb4b4b4{ Te6edf3addTb4b4b4(Ta5d6ff"Ta5d6ffimplementationTa5d6ff"Tb4b4b4, Tb4b4b4.Tb4b4b4.Tb4b4b4.Tb4b4b4) Tb4b4b4} T8b949e// Global!
Tb4b4b4}

T8b949e// RIGHT: Scoped to specific source set/module type
Te6edf3commonTestTb4b4b4.Te6edf3dependencies Tb4b4b4{ Te6edf3implementationTb4b4b4(Tb4b4b4.Tb4b4b4.Tb4b4b4.Tb4b4b4) Tb4b4b4}
Ta5d6ff```

Tc9d1d9### ❌ **Mistake: Extension type mismatch**
Ta5d6ff```Ta5d6ffkotlin
T8b949e// WRONG: LibraryExtension isn't a subtype of ApplicationExtension
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3ApplicationExtensionTff7b72> Tb4b4b4{
T8b949e// Won't apply to library modules
Tb4b4b4}

T8b949e// RIGHT: Use CommonExtension or specific types
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3CommonExtensionTff7b72> Tb4b4b4{
T8b949e// Applies to both
Tb4b4b4}
Ta5d6ff```

Tc9d1d9### ❌ **Mistake: Side effects during configuration**
Ta5d6ff```Ta5d6ffkotlin
T8b949e// WRONG: Eager task configuration at plugin-apply time
Te6edf3tasksTb4b4b4.Te6edf3withTypeTff7b72<Te6edf3TestTff7b72> Tb4b4b4{
T8b949e// Can realize tasks too early
Tb4b4b4}

T8b949e// RIGHT: Lazy, configuration-cache-friendly wiring
Te6edf3tasksTb4b4b4.Te6edf3withTypeTff7b72<Te6edf3TestTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3configureEach Tb4b4b4{
T8b949e// Applies to existing and future tasks lazily
Tb4b4b4}
Ta5d6ff```

Tc9d1d9## Related Files

Tff7b72- Ta5d6ff`AGENTS.md` - Development guidelines (Section 3.B testing, Section 4.A build protocol)
Tff7b72- Ta5d6ff`build-logic/convention/build.gradle.kts` - Convention plugin build config

Served by rngit 1.5.2 - Generated in 0.13s